/**
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 03/04/2013
 *
 */
package br.com.vfcfw.controller.util;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.application.FacesMessage;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.component.UIPanel;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.Flash;
import javax.faces.context.ResponseWriter;
import javax.faces.model.SelectItem;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import br.com.vfcfw.commons.exception.BusinessException;
import br.com.vfcfw.commons.util.ConstantesArquitetura;
import br.com.vfcfw.commons.util.LogUtil;
import br.com.vfcfw.commons.util.MensagemUtil;
import br.com.vfcfw.commons.util.ReflexaoUtil;

/**
 * Classe utilitária para abstrair a complexidade de algumas tarefas no JSF.
 * 
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 03/04/2013
 * 
 */
public class JSFUtil {

    /** {@link Logger} de {@link JSFUtil}. */
    private static final Logger LOGGER = Logger.getLogger(JSFUtil.class);

    /**
     * Adiciona uma mensagem de erro.
     * 
     * @param mensagem Mensagem.
     */
    public static void adicionarMensagemErro(String mensagem) {

        adicionarMensagemErro(null, mensagem);
    }

    /**
     * Adiciona uma mensagem de erro.
     * 
     * @param idComponente Identificador do componente (<tt>idForm:idComponente</tt>).
     * @param mensagem Mensagem.
     */
    public static void adicionarMensagemErro(String idComponente, String mensagem) {

        FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, mensagem, mensagem);
        getFacesContext().addMessage(idComponente, facesMessage);
    }

    /**
     * Adiciona uma mensagem de informação.
     * 
     * @param mensagem Mensagem.
     */
    public static void adicionarMensagemInformacao(String mensagem) {

        FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_INFO, mensagem, mensagem);
        getFacesContext().addMessage(null, facesMessage);
    }

    /**
     * Retorna o {@link UIComponent} correspondente ao Id informado.
     * 
     * @param id Identificador do componente (<tt>Se o <b>prependId</b> não estiver definido para <i>false</i>, deve-se
     *        informar o ID do formulário junto com o ID do componente</tt>).
     * @return {@link UIComponent}.
     */
    public static UIComponent obterComponentePorId(String id) {

        return getFacesContext().getViewRoot().findComponent(id);
    }

    /**
     * Limpa os valores submetidos dos componentes de edição contidos no componente informado (<tt>preferencialmente
     * associar a um {@link UIForm} ou a um {@link UIPanel}</tt>).
     * 
     * @param id id Identificador do componente (<tt>Se o <b>prependId</b> não estiver definido para <i>false</i>,
     *        deve-se informar o ID do formulário junto com o ID do componente</tt>), que terá os valores submetidos
     *        limpos.
     */
    public static void limparValoresSubmetidos(String id) {

        limparValoresSubmetidos(obterComponentePorId(id));
    }

    /**
     * Limpa os valores submetidos dos componentes de edição contidos no componente informado (<tt>preferencialmente
     * associar a um {@link UIForm} ou a um {@link UIPanel}</tt>).
     * 
     * @param componente {@link UIComponent} Componente que terá os valores submetidos limpos.
     */
    public static void limparValoresSubmetidos(UIComponent componente) {

        if (componente instanceof EditableValueHolder) {

            EditableValueHolder editableValueHolder = (EditableValueHolder) componente;
            editableValueHolder.setSubmittedValue(null);
            editableValueHolder.setValue(null);
            editableValueHolder.setLocalValueSet(false);
            editableValueHolder.setValid(true);
        }

        if (componente != null && componente.getChildCount() > 0) {

            for (UIComponent componenteFilho : componente.getChildren()) {
                limparValoresSubmetidos(componenteFilho);
            }
        }
    }

    /**
     * Retorna o caminho do contexto da aplicação.
     * 
     * @return Caminho do contexto.
     */
    public static String getContextPath() {

        return getExternalContext().getRequestContextPath();
    }

    /**
     * Retorna o caminho absoluto de um recurso baseado em seu caminho relativo.
     * 
     * @param caminhoRelativo Caminho relativo do recurso.
     * @return Caminho absoluto do recurso.
     */
    public static String getRealPath(String caminhoRelativo) {

        return getExternalContext().getRealPath(caminhoRelativo);
    }

    /**
     * Retorna um parâmetro armazenado no {@link Map} do Request.
     * 
     * @param chave Chave do parâmetro.
     * @return Valor do parâmetro.
     */
    public static String getParametroRequest(String chave) {

        return getRequestParameterMap().get(chave);
    }

    /**
     * Armazena um parâmetro no {@link Map} do Request.
     * 
     * @param chave Chave do parâmetro.
     * @param valor Valor do parâmetro.
     */
    public static void setParametroRequest(String chave, String valor) {

        getRequestParameterMap().put(chave, valor);
    }

    /**
     * Retorna um parâmetro armazenado no {@link Map} da {@link Flash}.
     * 
     * @param chave Chave do parâmetro.
     * @return Valor do parâmetro.
     */
    public static Object getParametroFlash(String chave) {

        return getFlash().get(chave);
    }

    /**
     * Armazena um parâmetro no {@link Map} do {@link Flash}.
     * 
     * @param chave Chave do parâmetro.
     * @param valor Valor do parâmetro.
     */
    public static void setParametroFlash(String chave, Object valor) {

        getFlash().put(chave, valor);
    }

    /**
     * Retorna um parâmetro armazenado no {@link Map} da View.
     * 
     * @param chave Chave do parâmetro.
     * @return Valor do parâmetro.
     */
    public static Object getParametroView(String chave) {

        return getViewMap().get(chave);
    }

    /**
     * Armazena um parâmetro no {@link Map} da View.
     * 
     * @param chave Chave do parâmetro.
     * @param valor Valor do parâmetro.
     */
    public static void setParametroView(String chave, Object valor) {

        getViewMap().put(chave, valor);
    }

    /**
     * Retorna um parâmetro armazenado no {@link Map} da Session.
     * 
     * @param chave Chave do parâmetro.
     * @return Valor do parâmetro.
     */
    public static Object getParametroSession(String chave) {

        return getSessionMap().get(chave);
    }

    /**
     * Armazena um parâmetro no {@link Map} do Session.
     * 
     * @param chave Chave do parâmetro.
     * @param valor Valor do parâmetro.
     */
    public static void setParametroSession(String chave, Object valor) {

        getSessionMap().put(chave, valor);
    }

    /**
     * Remove um parâmetro no {@link Map} do Session.
     * 
     * @param chave Chave do parâmetro.
     */
    public static void removerParametroSession(String chave) {

        getSessionMap().remove(chave);
    }

    /**
     * Retorna o {@link Map} do Request.
     * 
     * @return {@link Map} do Request.
     */
    public static Map<String, String> getRequestParameterMap() {

        return getExternalContext().getRequestParameterMap();
    }

    /**
     * Retorna o {@link HttpServletRequest}.
     * 
     * @return {@link HttpServletRequest}.
     */
    public static HttpServletRequest getRequest() {

        return (HttpServletRequest) getExternalContext().getRequest();
    }

    /**
     * Retorna o {@link Flash}.
     * 
     * @return {@link Flash}.
     */
    public static Flash getFlash() {

        return getExternalContext().getFlash();
    }

    /**
     * Retorna o {@link Map} da View.
     * 
     * @return {@link Map}.
     */
    public static Map<String, Object> getViewMap() {

        return getFacesContext().getViewRoot().getViewMap();
    }

    /**
     * Retorna o {@link Map} da Session.
     * 
     * @return {@link Map} da Session.
     */
    public static Map<String, Object> getSessionMap() {

        return getExternalContext().getSessionMap();
    }

    /**
     * Retorna um bean que esteja no {@link FacesContext}.
     * 
     * @param nomeBean Nome do bean.
     * @return Bean.
     */
    public static Object getBean(String nomeBean) {

        FacesContext lFacesContext = FacesContext.getCurrentInstance();

        final Object bean = lFacesContext.getELContext().getELResolver()
            .getValue(lFacesContext.getELContext(), null, nomeBean);

        if (bean == null) {

            LogUtil.error(LOGGER, String.format(
                "Bean com o nome %s não foi encontrado. Verifique se o nome informado esta correto.", nomeBean));
        }
        return bean;
    }

    /**
     * Ordena uma lista de SelectItem com label do tipo {@link String}.
     * 
     * @param lista Lista de {@link SelectItem}.
     * @return Lista ordenada.
     */
    public static List<SelectItem> ordenaSelectItem(List<SelectItem> lista) {

        Comparator<SelectItem> comparator = new Comparator<SelectItem>() {

            public int compare(SelectItem item1, SelectItem item2) {

                return item1.getLabel().compareTo(item2.getLabel());
            }
        };

        Collections.sort(lista, comparator);

        return lista;
    }

    /**
     * Ordena lista de SelectItem com label numérico.
     * 
     * @param lista Lista de {@link SelectItem}.
     * @return Lista ordenada.
     */
    public static List<SelectItem> ordenaSelectItemNumerico(List<SelectItem> lista) {

        Comparator<SelectItem> comparator = new Comparator<SelectItem>() {

            public int compare(SelectItem item1, SelectItem item2) {

                return Long.valueOf(item1.getLabel()).compareTo(Long.valueOf(item2.getLabel()));
            }
        };

        Collections.sort(lista, comparator);

        return lista;
    }

    /**
     * Gera uma lista de {@link SelectItem} baseado na lista genérica.
     * 
     * @param lista {@link List} Fonte de dados para gerar a lista de {@link SelectItem}.
     * @param nomeAtributoValor Nome do atributo que será o valor do {@link SelectItem} (Se estiver nulo o próprio
     *        objeto será o valor).
     * @param nomeAtributoLabel Nome do atributo que será o label do {@link SelectItem}.
     * @param usarArquivoRecurso <tt>TRUE</tt> usar arquivo de recurso no label, <tt>FALSE</tt> caso contrário.
     * @return Lista de {@link SelectItem}.
     * @throws Exception
     */
    public static <T> List<SelectItem> gerarSelectItems(List<T> lista, String nomeAtributoValor,
        String nomeAtributoLabel, boolean usarArquivoRecurso) {

        List<SelectItem> selectItens = null;

        if (lista == null || lista.isEmpty()) {
            selectItens = Collections.emptyList();
        }

        selectItens = new ArrayList<SelectItem>();

        Object valor = null;
        String label = null;

        if (lista != null) {

            for (T item : lista) {

                try {

                    valor = nomeAtributoValor == null ? item : ReflexaoUtil.executarMetodoGet(item.getClass(),
                        nomeAtributoValor, item);
                } catch (Exception e) {
                    throw new BusinessException(MensagemUtil.obterMensagem(getLocale(),
                        ConstantesArquitetura.MSG_ERRO_REFLEXAO_OBTER_VALOR, nomeAtributoValor), e);
                }

                try {

                    Object objLabel = ReflexaoUtil.executarMetodoGet(item.getClass(), nomeAtributoLabel, item);
                    label = (objLabel != null) ? objLabel.toString() : "";

                    if (usarArquivoRecurso) {

                        label = MensagemUtil.obterMensagem(getLocale(), label);
                    }

                } catch (Exception e) {
                    throw new BusinessException(MensagemUtil.obterMensagem(getLocale(),
                        ConstantesArquitetura.MSG_ERRO_REFLEXAO_OBTER_VALOR, nomeAtributoLabel), e);
                }

                selectItens.add(new SelectItem(valor, label));
            }
        }

        return selectItens;
    }

    /**
     * Gera um Map de itens para ser usado em combos baseado na lista genérica.
     * 
     * @param lista {@link List} Fonte de dados para gerar a lista de {@link SelectItem}.
     * @param nomeAtributoValor Nome do atributo que será o valor do {@link SelectItem} (Se estiver nulo o próprio
     *        objeto será o valor).
     * @param nomeAtributoLabel Nome do atributo que será o label do {@link SelectItem}.
     * @param usarArquivoRecurso <tt>TRUE</tt> usar arquivo de recurso no label, <tt>FALSE</tt> caso contrário.
     * @return Map de itens.
     * @throws Exception
     */
    public static <T> Map<Object, String> gerarMapSelectItems(List<T> lista, String nomeAtributoValor,
        String nomeAtributoLabel, boolean usarArquivoRecurso) {

        Map<Object, String> selectItens = new HashMap<Object, String>();

        Object valor = null;
        String label = null;

        if (lista != null) {

            for (T item : lista) {

                try {

                    valor = nomeAtributoValor == null ? item : ReflexaoUtil.executarMetodoGet(item.getClass(),
                        nomeAtributoValor, item);
                } catch (Exception e) {
                    throw new BusinessException(MensagemUtil.obterMensagem(getLocale(),
                        ConstantesArquitetura.MSG_ERRO_REFLEXAO_OBTER_VALOR, nomeAtributoValor), e);
                }

                try {

                    label = (String) ReflexaoUtil.executarMetodoGet(item.getClass(), nomeAtributoLabel, item);;

                    if (usarArquivoRecurso) {

                        label = MensagemUtil.obterMensagem(getLocale(), label);
                    }

                } catch (Exception e) {
                    throw new BusinessException(MensagemUtil.obterMensagem(getLocale(),
                        ConstantesArquitetura.MSG_ERRO_REFLEXAO_OBTER_VALOR, nomeAtributoLabel), e);
                }

                selectItens.put(valor, label);
            }
        }
        return selectItens;
    }

    /**
     * Método responsável por disponibilizar um determinado arquivo na forma de download.
     * 
     * @param nomeArquivo Nome do arquivo.
     * @param conteudoArquivo Conteúdo do arquivo.
     * @throws Exception
     */
    public static synchronized void downloadArquivo(final String nomeArquivo, final byte[] conteudoArquivo) {

        final HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance()
            .getExternalContext().getResponse();

        response.setHeader("Content-Disposition", String.format("attachment;filename=\"%s\"", nomeArquivo));
        response.setContentLength(conteudoArquivo.length);
        response.setContentType("application/x-download");

        OutputStream outputStream = null;

        try {

            outputStream = response.getOutputStream();
            outputStream.write(conteudoArquivo);
            FacesContext.getCurrentInstance().responseComplete();
        } catch (Exception e) {

            throw new BusinessException(
                MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_DOWNLOAD, nomeArquivo), e);
        } finally {

            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {
                throw new BusinessException(MensagemUtil.obterMensagem(ConstantesArquitetura.MSG_ERRO_DOWNLOAD,
                    nomeArquivo), e);
            }
        }
    }

    /**
     * Retorna o login do usuário autenticado.
     * 
     * @return Login do usuário autenticado.
     */
    public static String obterUsuarioAutenticado() {

        return getExternalContext().getRemoteUser();
    }

    /**
     * Retorna o {@link Locale} da {@link UIViewRoot}.
     * 
     * @return Locale.
     */
    public static Locale getLocale() {

        Locale locale = null;

        if (getFacesContext() != null) {

            locale = getFacesContext().getViewRoot().getLocale();
        } else {

            locale = Locale.getDefault();
        }

        return locale;
    }

    /**
     * Especifica o Locale na {@link UIViewRoot}.
     * 
     * @param locale {@link Locale}.
     */
    public static void setLocale(Locale locale) {

        getFacesContext().getViewRoot().setLocale(locale);
    }

    /**
     * Retorna o {@link ExternalContext}.
     * 
     * @return {@link ExternalContext}.
     */
    public static ExternalContext getExternalContext() {

        return getFacesContext().getExternalContext();
    }

    /**
     * Retorna a instância corrente do {@link FacesContext}.
     * 
     * @return {@link FacesContext}.
     */
    public static FacesContext getFacesContext() {

        return FacesContext.getCurrentInstance();
    }

    /**
     * Retorna o endereço IP da requisição.
     * 
     * @return Endereço IP.
     */
    public static String obterEnderecoIP() {

        return ((HttpServletRequest) getExternalContext().getRequest()).getRemoteAddr();
    }

    /**
     * Retorna o endereço MAC da host da requisição.
     * 
     * @return Endereço MAC.
     */
    public static String obterEnderecoMAC() {

        String enderecoMAC = null;

        try {

            String ip = obterEnderecoIP();
            InetAddress address = InetAddress.getByName(ip);
            NetworkInterface network = NetworkInterface.getByInetAddress(address);
            byte[] mac = network.getHardwareAddress();

            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < mac.length; i++) {
                sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : ""));
            }

            enderecoMAC = sb.toString();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return enderecoMAC;
    }

    /**
     * Redireciona a aplicação para uma determinada página.
     * 
     * @param pagina Página para a qual será redirecionada.
     */
    public static void redirecionaPagina(String pagina) {

        FacesContext facesContext = getFacesContext();
        ExternalContext ec = getExternalContext();

        try {
            if (ec.isResponseCommitted()) {

                // redirect não é possível nessa situação
                return;
            }

            // fix para renderer kit (Mojarra's e PrimeFaces's ajax redirect)
            if ((facesContext.getPartialViewContext().isPartialRequest())
                && facesContext.getResponseWriter() == null
                && facesContext.getRenderKit() == null) {
                ServletResponse response = (ServletResponse) ec.getResponse();
                ServletRequest request = (ServletRequest) ec.getRequest();
                response.setCharacterEncoding(request.getCharacterEncoding());

                RenderKitFactory factory = (RenderKitFactory) FactoryFinder
                    .getFactory(FactoryFinder.RENDER_KIT_FACTORY);

                RenderKit renderKit = factory.getRenderKit(facesContext, facesContext.getApplication().getViewHandler()
                    .calculateRenderKitId(facesContext));

                ResponseWriter responseWriter = renderKit.createResponseWriter(response.getWriter(), null,
                    request.getCharacterEncoding());
                facesContext.setResponseWriter(responseWriter);
            }

            ec.redirect(ec.getRequestContextPath() + (pagina != null ? pagina : ""));

        } catch (IOException e) {
            throw new FacesException(e);
        }
    }

    /**
     * Retorna o nome do host informado.
     * 
     * @param numeroIP Endereço IP do host.
     * @return Nome do host.
     */
    public static String obterNomeHost(String numeroIP) {

        String nomeHost = null;

        try {

            InetAddress address = InetAddress.getByName(numeroIP);
            nomeHost = address.getHostName();

        } catch (Exception e) {
            LogUtil.error(LOGGER, e, "Erro ao obter o nome do host.");
        }
        return nomeHost;
    }

}
